// 指定包名，管理服务层类的归属
package com.example.ossmultipart.service; // OSS 分片上传服务类所在包

// 导入阿里云 OSS SDK 相关类
import com.aliyun.oss.OSS; // OSS 客户端接口
import com.aliyun.oss.model.CompleteMultipartUploadRequest; // 完成分片上传请求类
import com.aliyun.oss.model.CompleteMultipartUploadResult; // 完成分片上传结果类
import com.aliyun.oss.model.InitiateMultipartUploadRequest; // 初始化分片上传请求类
import com.aliyun.oss.model.InitiateMultipartUploadResult; // 初始化分片上传结果类
import com.aliyun.oss.model.ObjectMetadata; // 对象元数据类
import com.aliyun.oss.model.PartETag; // 分片标识（包含ETag与分片号）
import com.aliyun.oss.model.UploadPartRequest; // 上传分片请求类
import com.aliyun.oss.model.UploadPartResult; // 上传分片结果类
import com.aliyun.oss.model.ListPartsRequest; // 列举分片请求类
import com.aliyun.oss.model.PartListing; // 分片列表结果
import com.aliyun.oss.model.PartSummary; // 分片摘要信息

// 导入 Spring 组件与依赖注入注解
import org.springframework.beans.factory.annotation.Autowired; // 依赖注入注解
import org.springframework.stereotype.Service; // 服务类注解
import org.slf4j.Logger; // 日志记录器
import org.slf4j.LoggerFactory; // 日志记录器工厂

// 导入其他工具类与数据结构
import java.io.InputStream; // 输入流类，用于接收分片数据
import java.time.LocalDate; // 日期类，用于目录组织
import java.util.*; // 导入集合与工具类

// 导入自定义配置属性类
import com.example.ossmultipart.config.AliYunOssProperties; // OSS 属性配置类

// 服务类，封装 OSS 分片上传的初始化、分片上传、完成与终止逻辑
@Service // 声明为 Spring 的服务组件
public class OssMultipartService { // 定义分片上传服务类
    private static final Logger log = LoggerFactory.getLogger(OssMultipartService.class); // 创建日志记录器

    // 注入 OSS 客户端，用于与阿里云交互
    @Autowired // 从容器中注入 OSS 客户端
    private OSS ossClient; // OSS 客户端实例

    // 注入属性配置，用于获取桶名、基础目录等信息
    @Autowired // 从容器中注入属性配置
    private AliYunOssProperties properties; // OSS 属性配置实例

    // 初始化分片上传，返回 uploadId 与对象名
    public Map<String, String> initiateMultipartUpload(String originalFilename, String contentType) { // 初始化分片上传方法
        // 生成唯一的对象名，包含基础目录/日期/随机UUID_原文件名，便于分类与防重名
        String objectName = buildObjectName(originalFilename); // 生成对象名

        // 创建初始化请求，设置桶名与对象名
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(
                properties.getBucketName(), // 目标桶名
                objectName // 对象名（路径）
        ); // 初始化请求对象

        // 设置元数据（可选），至少设置 content-type，便于浏览器或消费端识别
        ObjectMetadata metadata = new ObjectMetadata(); // 创建对象元数据
        if (contentType != null && !contentType.isEmpty()) { // 若前端提供了内容类型
            metadata.setContentType(contentType); // 设置内容类型
        } // if 结束
        request.setObjectMetadata(metadata); // 将元数据绑定到初始化请求

        // 调用 OSS 客户端初始化分片上传，获取全局唯一的 uploadId
        InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request); // 发起初始化
        String uploadId = result.getUploadId(); // 获取 uploadId
        log.info("Initiated multipart upload: bucket={}, objectName={}, uploadId={}", properties.getBucketName(), objectName, uploadId); // 打印初始化信息

        // 返回 uploadId 与 objectName，供前端后续分片上传与合并使用
        Map<String, String> resp = new HashMap<>(); // 创建返回 Map
        resp.put("uploadId", uploadId); // 放入 uploadId
        resp.put("objectName", objectName); // 放入对象名
        return resp; // 返回结果
    } // 方法结束

    // 上传单个分片，返回该分片的 ETag（供合并时校验与排序）
    public String uploadPart(String uploadId, String objectName, int partNumber, InputStream inputStream, long partSize) throws Exception { // 上传分片方法
        // 构造上传分片请求，设置必要参数（桶、对象、uploadId、分片号）
        UploadPartRequest uploadPartRequest = new UploadPartRequest(); // 创建上传分片请求
        uploadPartRequest.setBucketName(properties.getBucketName()); // 设置桶名
        uploadPartRequest.setKey(objectName); // 设置对象名
        uploadPartRequest.setUploadId(uploadId); // 设置 uploadId
        uploadPartRequest.setPartNumber(partNumber); // 设置分片号（1~10000）
        uploadPartRequest.setInputStream(inputStream); // 设置分片数据流
        uploadPartRequest.setPartSize(partSize); // 设置本分片大小（字节数）

        // 执行上传分片，获取结果与 ETag（服务端计算的MD5标识）
        UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest); // 执行上传调用
        PartETag partETag = uploadPartResult.getPartETag(); // 获取分片标识
        log.info("Uploaded part: bucket={}, objectName={}, uploadId={}, partNumber={}, partSize={}, eTag={}",
                properties.getBucketName(), objectName, uploadId, partNumber, partSize, partETag.getETag()); // 打印分片上传信息
        return partETag.getETag(); // 返回服务端返回的 ETag
    } // 方法结束

    // 完成分片上传，服务端合并所有分片为完整文件
    public String completeMultipartUpload(String uploadId, String objectName, List<PartETag> partETags) { // 完成分片上传方法
        // 注意：必须保证 partETags 中的分片号与 ETag 正确，OSS 会自行按分片号排序
        CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
                properties.getBucketName(), // 设置桶名
                objectName, // 设置对象名
                uploadId, // 设置 uploadId
                partETags // 设置分片集合
        ); // 创建完成请求

        // 调用 OSS API 完成分片合并，返回结果包含最终 ETag
        CompleteMultipartUploadResult completeResult = ossClient.completeMultipartUpload(completeRequest); // 执行合并
        log.info("Completed multipart upload: bucket={}, objectName={}, uploadId={}, finalETag={}",
                properties.getBucketName(), objectName, uploadId, completeResult.getETag()); // 打印合并结果
        return completeResult.getETag(); // 返回最终对象的 ETag（非必须，仅供参考）
    } // 方法结束

    // 终止（取消）分片上传，清理服务器端已上传的分片，避免存储占用
    public void abortMultipartUpload(String uploadId, String objectName) { // 终止分片上传方法
        // 调用客户端的 abort 方法；注意：OSS SDK abort 接口在 UploadPartRequest 之外提供
        ossClient.abortMultipartUpload(new com.aliyun.oss.model.AbortMultipartUploadRequest(
                properties.getBucketName(), // 设置桶名
                objectName, // 设置对象名
                uploadId // 设置 uploadId
        )); // 执行终止请求
        log.warn("Aborted multipart upload: bucket={}, objectName={}, uploadId={}", properties.getBucketName(), objectName, uploadId); // 打印终止信息
    } // 方法结束

    // 校验上传会话是否存在（通过列举分片，若能成功列举则会话存在）
    public boolean verifyUploadSession(String uploadId, String objectName) { // 校验会话是否存在
        try {
            ListPartsRequest listReq = new ListPartsRequest(properties.getBucketName(), objectName, uploadId); // 创建列举请求
            listReq.setMaxParts(1); // 仅需尝试获取 1 个分片确认存在性
            PartListing listing = ossClient.listParts(listReq); // 执行列举
            boolean exists = listing != null; // 能拿到结果即视为存在
            log.info("Verify upload session: bucket={}, objectName={}, uploadId={}, exists={}",
                    properties.getBucketName(), objectName, uploadId, exists);
            return exists;
        } catch (Exception e) {
            log.warn("Verify upload session failed: bucket={}, objectName={}, uploadId={}, error={}",
                    properties.getBucketName(), objectName, uploadId, e.getMessage());
            return false; // 异常视为不存在
        }
    } // 方法结束

    // 从服务端列举已上传分片，构造 PartETag 列表（用于更稳健的合并）
    public List<PartETag> listServerPartETags(String uploadId, String objectName) { // 服务端列举分片
        List<PartETag> etags = new ArrayList<>(); // 初始化列表
        ListPartsRequest listReq = new ListPartsRequest(properties.getBucketName(), objectName, uploadId); // 创建列举请求
        PartListing listing;
        do { // 处理分页列举
            listing = ossClient.listParts(listReq); // 执行列举
            for (PartSummary ps : listing.getParts()) { // 遍历分片摘要
                etags.add(new PartETag(ps.getPartNumber(), ps.getETag())); // 收集 PartETag
            }
            listReq.setPartNumberMarker(listing.getNextPartNumberMarker()); // 设置下一页标记
        } while (listing.isTruncated()); // 若被截断则继续列举
        etags.sort(Comparator.comparingInt(PartETag::getPartNumber)); // 按分片号排序
        log.info("List server parts: bucket={}, objectName={}, uploadId={}, count={}",
                properties.getBucketName(), objectName, uploadId, etags.size()); // 打印列举数量
        return etags; // 返回结果
    } // 方法结束

    // 构造对象名：baseFolder/yyyy-MM-dd/uuid_originalFilename
    private String buildObjectName(String originalFilename) { // 构造对象名辅助方法
        String safeName = originalFilename == null ? "file" : originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_"); // 过滤不安全字符
        String dateDir = LocalDate.now().toString(); // 获取当前日期作为目录（yyyy-MM-dd）
        String uuid = UUID.randomUUID().toString(); // 生成随机 UUID 防止重名
        return String.join("/", // 使用斜杠拼接路径
                Optional.ofNullable(properties.getBaseFolder()).orElse("uploads"), // 基础目录前缀
                dateDir, // 日期目录
                uuid + "_" + safeName // 文件名：uuid_原名
        ); // 返回对象名
    } // 方法结束
} // 服务类结束